package se.plingapp.entitybeans;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

/**
 * User persistence.
 *
 * @author Erik Brännström
 * @author Jens Ljungblad
 */
@Entity
@NamedQuery(name = "findAllUsers", query = "SELECT u FROM User u")
@Table(name = "users")
public class User implements Serializable {
    
    /**
     * Different types of user roles.
     */
    public enum UserRole { USER, ADMIN }

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "user_role", nullable = false)
    private UserRole userRole;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String name;

    @Column(name = "is_public", nullable = false)
    private boolean isPublic;

    @Column(name = "created_on", updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private Date registrationDate;

    @Column(name = "last_login")
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastLogin;

    @ManyToMany
    @JoinTable(name = "followers",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "follower_id"))
    private List<User> follows;

    @ManyToMany(mappedBy = "likes", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
    private List<Content> likes;

    /** 
     * Empty constructor. Set default values.
     */
    public User() {
        this.userRole = UserRole.USER;
        this.isPublic = false;
        this.registrationDate = new Date();
        this.likes = new ArrayList<Content>();
    }

    /**
     * Create a new user, only used for unit testing.
     *
     * @param email
     * @param password
     * @param name
     */
    public User(String email, String password, String name) {
        this();
        this.email = email;
        this.password = this.hashPassword(password);
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public UserRole getUserRole() {
        return userRole;
    }

    public void setUserRole(UserRole userRole) {
        this.userRole = userRole;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.setPassword(password, true);
    }

    public void setPassword(String password, boolean hash) {
        if(hash) {
            this.password = this.hashPassword(password);
        } else {
            this.password = password;
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean getIsPublic() {
        return isPublic;
    }

    public void setIsPublic(boolean isPublic) {
        this.isPublic = isPublic;
    }

    public Date getRegistrationDate() {
        return registrationDate;
    }

    public void setRegistrationDate(Date registrationDate) {
        this.registrationDate = registrationDate;
    }

    public Date getLastLogin() {
        return lastLogin;
    }

    public void setLastLogin(Date lastLogin) {
        this.lastLogin = lastLogin;
    }

    public List<User> getFriends() {
        return this.follows;
    }

    public void setFriends(List<User> follows) {
        this.follows = follows;
    }

    public List<Content> getLikes() {
        return likes;
    }

    public void setLikes(List<Content> likes) {
        this.likes = likes;
    }

    /**
     * Return true if the user is an administrator.
     */
    public boolean isAdmin() {
        if (this.userRole == UserRole.ADMIN) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Like a piece of content.
     *
     * @param content
     */
    public void like(Content content) {
        content.addLike(this);
        this.likes.add(content);
    }

    /**
     * Unlike a piece of content.
     *
     * @param content
     */
    public void unlike(Content content) {
        content.removeLike(this);
        this.likes.remove(content);
    }

    /**
     * Return true if this user follows the given user.
     * 
     * @param user
     */
    public boolean follows(User user) {
        return this.follows.contains(user);
    }

    /**
     * Return true if this user is followed by the given user.
     *
     * @param user
     */
    public boolean isFollowedBy(User user) {
        return user.getFriends().contains(this);
    }

    /**
     * Follow a user.
     *
     * @param user
     */
    public void follow(User user) {
        this.follows.add(user);
    }

    /**
     * Unfollow a user.
     *
     * @param user
     */
    public void unfollow(User user) {
        this.follows.remove(user);
    }

    /**
     * Return large gravatar image url.
     */
    public String getLargeGravatar() {
        String hash = this.hashString(this.email);
        return "http://www.gravatar.com/avatar/" + hash + "?s=200";
    }

    /**
     * Return small gravatar image url.
     */
    public String getSmallGravatar() {
        String hash = this.hashString(this.email);
        return "http://www.gravatar.com/avatar/" + hash + "?s=80";
    }

    /**
     * Hash a password string using the SHA-1 algorithm for security purposes.
     *
     * @param password
     */
    private String hashPassword(String password) {
        try {
            MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
            sha1.update(password.getBytes("UTF-8"));
            BigInteger hash = new BigInteger(1, sha1.digest());
            return hash.toString(16);
        } catch (NoSuchAlgorithmException e) {
            throw new NullPointerException("No such algorithm.");
        } catch (UnsupportedEncodingException e) {
            throw new NullPointerException("No such character set.");
        }
    }

    /**
     * Hash a string using the MD5 algorithm.
     *
     * @param string
     */
    private String hashString(String string) {
        try {
            MessageDigest sha1 = MessageDigest.getInstance("MD5");
            sha1.update(string.getBytes("UTF-8"));
            BigInteger hash = new BigInteger(1, sha1.digest());
            return hash.toString(16);
        } catch (NoSuchAlgorithmException e) {
            throw new NullPointerException("No such algorithm.");
        } catch (UnsupportedEncodingException e) {
            throw new NullPointerException("No such character set.");
        }
    }

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object object) {
        if (!(object instanceof User)) {
            return false;
        }
        User other = (User) object;
        if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "User[id=" + id + ", name=" + name + "]";
    }
}
